home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Windows Expert
/
Windows Expert.iso
/
program
/
wintech1.zip
/
SHAW.ZIP
/
COLUMN.TXT
next >
Wrap
Text File
|
1991-12-23
|
29KB
|
630 lines
THE VIRTUAL COLUMN
BY RICHARD HALE SHAW
GETTING A HANDLE ON DDE
Windows 3.1 includes DDEML.DLL, a set of routines for creating
and managing DDE conversations. This column explores the library
and presents C++ objects that make DDE simpler and easier than
ever.
This column explores the use of C++ as a Windows development
language. It does so in two ways: first, C++ as *the*
development language for Windows, largely usurping the role of
C; and second, C++ as an object-oriented solution to a
non-object-oriented environment. Let me explain.
First, I believe that C will no longer be the dominant
programming language for Windows developers. I think a number of
developers will find that they can solve their Windows
programming problems with Visual Basic, Turbo Pascal for Windows
or environments like Actor, ToolBook or SmallTalk. But I think
that most Windows programmers who currently use C will move to
C++ over the next 18 months.
Second, Windows -- by any classical definition of OOP -- is
*not* an OOP environment. Sure: you can use a window to tie data
and code (a window procedure) together, and there's simple
(primitive?) sub-classing. But data encapsulation is relatively
poor compared to other OOP solutions like C++. Plus, when it
comes to inheritance and polymorphism, forget it: Windows
doesn't even come close to offering capabilities like these.
C++, however, offers these OOP features along with the power of
C, and, I believe, is the best tool for creating new Windows
programs.
Consequently, this column will explore Windows programming from
a C++ perspective, and we'll use C++ to create unique solutions
to Windows programming problems. As we proceed, don't hesitate
to send me your thoughts on these issues or on specific ideas
that the column addresses; I'll be sure to share them with
everyone as space permits.
With this in mind, let's look at the first column: on Windows
DDE.
If you've ever written a program that uses Windows' Dynamic Data
Exchange (DDE), or even attempted to, you know how trying and
frustrating it is. DDE -- or as my friend and colleague Charles
Petzold calls it, "the protocol from Hell" -- is one of the more
exasperating facilities in Windows. The description of DDE in
the Windows SDK leaves more to the imagination than to fact, and
the only way to be sure your DDE program worked
properly has been to test it against Microsoft products that
implemented it (after all, we all figure that those guys will
get it right). Unfortunately, this isn't always the case,
either. But many of us have tried, if desparately, to
implement DDE correctly, because the benefit to an
application are so worthwhile.
Fortunately, there's hope. Windows 3.1 now includes DDEML.DLL: a
DLL that packages a number of routines for creating and managing
DDE conversations. This DDL makes DDE infinitely simpler to
implement in your programs, and reduces the possibility of do so
incorrectly. Plus, you can even use DDEML.DLL under
Windows 3.0.
This article provides an overview of the Windows 3.1 DDEML.DLL
and shows you how to use it in your programs. If you've used DDE
"in the raw" before, you'll have some 'unlearning' to do first.
If you've never programmed DDE before you're in for a treat:
with the DDEML, DDE has never been easier. We'll begin with a
brief overview of what DDE is and how it works. Then, I'll
present you with highlights of the DDE Management library, its
features and how it works. Finally, I'll conclude with some C++
objects that utilize the DDEML.
Keep in mind that Windows 3.1 was still in beta as this column
was written, and the only documentation on the DDEML is in an
on-line file. Further, version of Borland C++ that I'm using
works appears to work with Windows 3.1, but it doesn't do so
officially. The consequence of all this is: there's no absolute
guarantee that the code examples will work without modification
once Windows 3.1 officially arrives. Fortunately, you'll be able
to download an updated version of the code from WTJ at that
time.
An Overview of Traditional DDE
As you're probably aware, DDE lets applications share data. Like
the Clipboard, DDE operations are often initiated by
the user via an application menu and can transfer data on a one-
time basis. But unlike the Clipboard, DDE often continues
without user interaction, and it can create on-going data
transfers.
In DDE terminology, the application that initiates the transfer
and requests the data is called the 'client' and the application
that provides the data is the 'server'. The client can
initiate a 'conversation' (i.e., establish a relationship) with
a client and request data from it.
DDE clients and servers 'converse' on specific contexts or
'topics' from which smaller data 'items' are exchanged. A topic
is usually the name of a file or a document, a database table, a
worksheet or an application-specific string. An item is a
specific block of text, a database row(s), a cell(s) or another
application-specific string. A conversation is uniquely
identified by combining the server name and the topic name. Data
items are distinguished by the server, topic and item names.
A DDE application can simultaneously engage in multiple
conversations with multiple DDE applications, on the same or on
multiple topics. An application can be a client in one
conversation and simultanelusly a server in another. A server
can supply data to more than one client at a time and a
client can request data from multiple servers at the same time.
Thus, 'AppA' can be a client of 'AppB' and 'AppC', and
simultaneously be a server to both 'AppB' and 'AppC'. The
only hitch is that you keep it all straight.
Traditionally speaking, DDE is a protocol: it establishes a set
of rules that determine what is supposed to happen, and when.
Unfortunately, some of these rules aren't terribly well
defined in the WinSDK (in fact, the DDEML docs are *not*
monuments to great clarity!), and consequently, it's been
difficult to figure out what a DDE application is supposed to do
in many circumstances. But the essence of a DDE
implementation is that a number of Windows messages are
provided, and these could be used to implement the protocol.
Since Windows doesn't offer any interprocess communication
features in the mature sense (i.e., shared memory, queues,
pipes, etc. as will be found in Windows NT), DDE applications
use messages (prefixed with 'WM_DDE_') to notify other
applications of their intentions, and then transfer data via the
global atom table. This table is a collection of strings that
any Windows application can register with the system, and which
any Windows application can access. Thus to transfer data, a
server application puts the data into the atom table, obtains a
handle to the data, includes the handle as a parameter with the
appropriate DDE message, and sends the latter to the client. The
receiving application processes the message and uses the
included handle to retrieve the data from the atom table.
Finally, since every DDE conversation takes place between two
windows (identified in the conversation by their window handles)
most Windows developers dedicate an invisible window to every
DDE conversation. This is particularly helpful if an application
engages in more than one conversation at a time. This requires
creating the new window and writing a window procedure that can
respond to the appropriate DDE messages. (Interestingly, many
Windows developers call these 'object windows' and did so long
before the Borland application framework of the same name
appeared.)
The DDE Managment Library
Under the hood, the DDEML still uses the approach outlined
above: it communicates with DDE client and server applications
via the standard WM_DDE messages found in Windows. And, it
transfers data between those applications by using the global
atom table. For these reasons, an application that uses DDEML
will be completely compatible with DDE applications written
without the benefit of DDEML.
However, DDEML applications needn't process WM_DDE_* messages or
access the atom table. Instead, the DDEML provides a set of API
functions, along with a few structures and special messages
known as 'transactions'. These facilities serve to hide the
details of traditional DDE conversations. So, your application
need only make a few function calls, and be designed to handle
the transaction messages when they are received.
While DDEML transactions are messages, they're not sent to your
application's window procedure. Instead, you must supply a
special function of your own that, like a window procedure, is
called by DDEML whenever your application receives a
transaction. This 'callback function' (or simply 'callback' for
short) must be capable of processing any transactions that your
application receives from DDEML. In addition, it must be
able to respond by posting transactions as replies and (if your
application is a DDE server) by supplying the appropriate data.
One of the nicer features of the DDEML is that you can filter
out unwanted and unneeded transactions, thus limiting the number
and types of transactions that have to be handled by your
application. These 'transaction filters' are completely dynamic:
you can change them on-the-fly -- even in the midst of a DDE
conversation. The consequence of this feature is that you can
optimize the performance of your DDE application: your callback
will never even receive the filtered messages, and the
sending application will receive a reply transaction indicating
that the transaction it sent wasn't processed (unless the
sending application itself filters out *these* transactions).
The DDEML lets you create DDE conversations that utilize both
synchronous *and* asynchronous transactions. And, the DDEML
supports all the traditional DDE conversational forms: Request,
Advise with notification (a.k.a., 'Warm Link'), Advise with Data
(a.k.a. 'Hot Link'), Execute and Poke. (In talks I've given at
SoftWare Development, I've had more than one student point out
the implicit sexual metaphor in the choice of these terms -- but
they're all legitimate, i.e., chosen by Microsoft.) In addition,
you can use the DDEML to create DDE Monitor, i.e., an
application that can monitor all the DDE conversations taking
place in the system. An example of just such a program is
DDESPY.EXE, which comes with the Windows 3.1 SDK.
Initializing DDEML
The DDEML functions fall into several categories. For instance,
there are functions for initializing the connection between your
application and the DDEML, and for establishing (or
terminating) DDE conversations. There are transaction management
functions for initiating a transaction and abandoning
asynchronous ones. Other API functions let you create global
data objects, copy data to them and free them. The entire DDEML
API is summarized in the table in Figure 1. You must #include
DDEML.H in the source files that reference them and link in
DDEML.LIB (DDEML.LIB is the import library for DDEML.DLL --
which should be on the PATH when you run the application).
Before it can call any other DDEML function, you must register
your application with the DDEML by calling DdeInitialize:
DWORD idInst = 0L;
FARPROC lpDdeProc;
WORD errval;
.
.
..
lpDdeProc = MakeProcInstance((FARPROC) DDECallback, hInst);
if(DdeInitialize((LPDWORD) &idInst,(PFNCALLBACK)lpDdeProc,
APPCMD_CLIENTONLY, 0L))
return FALSE;
DdeInitialize takes a pointer to a DWORD as its first parameter,
and sets this to an 'application-instance identifier'
-- essentially a handle that refers to that instance of the
task's usage of the DDEML (a task can have more than one
instance of DDEML). Without this handle, the DDEML wouldn't be
able to distinguish one usage of DDEML.DLL from another
by the same task, a problem if the application needs to use the
DDEML more than once at the same time. For instance, suppose an
application is using OLE (which uses DDE to link and embed
objects) while it's using the DDEML. The instance identifier
lets the DDEML keep the application's use of DDE in the OLE DLL
distinct from the application's own use of the DDEML.
Consequently, you have to retain the instance handle when
DdeInitialize returns, and pass it to a great number of the
DDEML API functions.
It's via DdeInitialize that you provide the address of your
application's callback (in the 2nd parameter). If your
application needs to establish DDE conversations with different
behaviors, you can call DdeInitialize for each type of
conversation, passing it a different callback function address,
and receiving a different instance handle each time. Each
instance handle is tied to the callback function passed with
each call to DdeInitialize.
You can also use DdeInitialize to set transaction filters (and
prevent the receipt of a variety of unwanted messages), as well
as specify whether the application is a DDE monitor and or needs
other services from the DDEML.
Server Naming Services
Once an application has registered itself with DDEML, it can
either initiate a conversation (if it's a DDE client) or
register its name and the topics it supports (if it's a DDE
server).
In traditional DDE, the name of a DDE server is based on the
application name. For example, the server name of EXCEL.EXE is
'Excel'. To access Excel as a server with traditional DDE, you
have to specify that name (or rather an atom handle for it) in
the WM_DDE_INITIATE sent by your client application. (Of course,
a client application can always use wildcards invoke
reponses from *all* the available servers.) Plus, there's no way
to know if Excel is available without first attempting to
converse with it.
With DDEML, a server can take on more than one name: the
DdeNameService function lets a DDE server register multiple
names with the system. This function will send an XTYPE_REGISTER
transaction to every DDEML application in the system (except
those that are filtering out this transaction), thus notifying
them that the new server is available. One advantage is that
client applications won't have to attempt to converse with a
server to find out if it's available: they'll be notified once
it's available.
Another advantage is that the server can be more efficient:
DdeNameService can toggle the server's filtering of
XTYPE_CONNECT transactions (which are sent to every server when
a client is trying to start a conversation -- regardless of the
server name specified by the client). Thus, after calling
DdeNameService, the server will only receive XTYPE_CONNECT
transactions when a client specifies one of the registered
server names. A server can also use DdeNameService to
*de-register* a server name: since this causes DDEML to send the
XTYP_UNREGISTER transaction to every DDEML client, client
applications will know when a server is no longer available.
A server can also use DdeNameService to facilitate aliasing: the
server can use different names to indicate different types of
data as it becomes available.
Creating A Conversation
A client application can establish a DDE conversation by calling
DdeConnect:
DWORD idInst;
HSZ hszServer;
HSZ hszTopic;
HCONV hConv;
.
.
.
hConv = DdeConnect(idInst,hszServer, hszTopic,(LPVOID)NULL);
This function sends an XTYP_CONNECT transaction to the specified
server and takes four parameters: the instance handle returned
from DdeInitialize, and two string handles (one for the server
name and the other for the topic name). Either or both of the
string handles can be NULL: as with traditional DDE, a NULL
server name handle will allow a connection with any available
server, and a NULL topic handle will allow a conversation on any
topic supported by the selected server.
The final parameter is a pointer to a CONVCONTEXT structure
(defined in DDEML.H). You can pass a NULL instead, and the
server will receive a default CONVCONTEXT structure along with
the XTYP_CONNECT transaction. The string handles are created by
calling DdeCreateStringHandle.
In traditional DDE, you have to create an invisible window
that's dedicated to managing the DDE conversation for you. If
DdeConnect finds a server with the specified name that supports
the specified topic, it will return a conversation handle: in
this implementation of DDEML, a handle to an invisible
window which DDEML creates for you. All messages (even WM_DDE_*
messages) sent to this window are handled by DDEML (it supplies
the window's window procedure) and then passed on to your
application's callback as DDEML transactions. Since there's no
guarantee that this approach will be used in future
implementations of DDEML, you can't rely on the conversation
handle to be a window handle. But you can still use the
conversation handle returned by DdeConnect to refer to a
particular conversation in other calls to the DDEML. And it's
vital for distinguishing between conversations if your
application is maintaining more than one DDEML conversation at a
time.
Note that the DDEML also provides DdeConnectList, which lets a
client application establish more than one conversation at a
time. And, an application can terminate either single or
multiple conversations with DdeDisconnect or DdeDisconnectList,
respectively.
Once an conversation has been established between a client and a
server, the client application can call DdeClientTransaction to
receive data from the server:
HCONV hConv;
HSZ hszItem;
HDDEDATA hData;
DWORD dwResult;
.
.
.
hData = DdeClientTransaction((LPBYTE)NULL,0,hConv,hszItem,
CF_TEXT,XTYP_ADVSTART,1000L,&dwResult);
This function lets a client application receive data on a
one-time basis (cold-link), receive notification of updates
(warm-link) or receive the updated data directly (hot-link). The
first two parameters are supplied in the event that the client
wants to pass data to the server (for the XTYP_EXECUTE or
XTYPE_POKE transactions), and specify a pointer to the data and
the data length. The third parameter is the conversation handle,
and the following one is a string handle that specifies the data
item being requested (like other string handles, created via
DdeCreateStringHandle). The fifth parameter specifies the
Clipboard data format being used (such as CF_TEXT).
The next parameter to DdeClientTransaction is the transaction
type, which can be: XTYP_ADVSTART (to initiate a warm or hot
link), XTYP_ADVSTOP (to terminate such a link), XTYP_EXECUTE (to
issue commands to the server), XTYP_POKE (to offer data to the
server) and XTYP_REQUEST (to make a one-time request for data).
The remaining two parameters specify a timeout value (the
maximum time in milliseconds that a client will wait for a
response from the server) and a result-flag variable. The bits
of the latter are set to designate the result of the
transaction.
If it's successful, DdeClientTransaction returns a handle to a
global memory object that contains the data sent from the
server (provided the transaction was one that requested data).
Your application can pass this HDDEDATA handle to DdeGetData to
retrieve the data:
HDDEDATA hData;
char szBuf[100];
.
.
.
DdeGetData(hData, (LPBYTE)szBuf, 100L, 0L);
In this example, DdeGetData will retrieve the data from a global
memory object (represented by hData) and place it in szBuf. The
last two parameters specify the number of bytes to copy from the
global object to the buffer and at what offset to begin copying.
If you don't know how big the data item is, you can pass a NULL
in place of the local buffer (in this case, szBuf), and
DdeGetData will return the size on bytes of the data item.
If this all sounds terribly complicated, it's not. The listings
accompanying this article show how to use these DDEML functions
in the context of a C++ object that makes a one-time data
request from a DDE server.
CallBack Functions
While the examples above didn't necessitate a callback on the
client side, they don't preclude the client from having one and
the server certainly needed one. If you haven't already
guessed, callbacks are a *lot* like window procedures. Both
consist of a large *switch* statement, process messages, and are
called by Windows, not your application. DDEML callbacks differ
from window procedures only in the number and types of their
arguments and the messages they process. Here, for example, is a
skeleton callback function:
HDDEDATA EXPENTRY DdeProc(WORD usType,WORD usFmt,HCONV hConv,
HSZ hsz1,HSZ hsz2,HDDEDATA hData,DWORD lData1,DWORD
lData2) {
.
.
.
switch(usType)
{
.
.
.
}
}
As you can see, there are eight arguments passed to a callback
by the DDEML. (It's too bad that Microsoft didn't just supply a
pointer to a structure and pass the structure address to the
callback -- it would have been simpler, easier and faster). The
transaction message is the first parameter, followed by the data
format and the conversation handle. Whether the remaining
parameters (two string handles, a data handle and two
transaction-specific double-words) are used depends on the
transaction that's passed.
Synchronous vs. Asynchronous Transactions
At this point, you should have a general idea of how DDEML
works. Before I present some C++ code examples for using it in
an application, there's on other feature of DDEML that's well
worth mentioning.
DDEML supports both asychronous and synchronous transactions.
The client can conform to the model of traditional DDE and issue
synchronous transactions where the client waits for the
transaction to be processed (as shown in the example using
DdeClientTransaction shown above). Or, it can issue an
asynchronous transactions: the client returns immediately
after calling DdeClientTransaction and proceeds normally; when
the server replies, the client's callback will receive the
response transaction.
Synchronous transactions are simpler, faster and easier
to use. But they can hold up the client if the server takes a
long time to process a transaction or is processing a high
volume of transactions for this and other clients. Asynchronous
transactions, while more complex and difficult to maintain,
allow the client more control while a transaction is in
progress; indeed, a client and go on to issue other transactions
to the same or other servers.
Using C++ with DDEML
To give you a 'taste' of what DDEML will be like (indeed, it
should be available and fully documented not long after this
article appears), I've written a few C++ classes that use it.
(Keep in mind that these won't be fully tested until after DDEML
is released, and that these classes should be considered 'works
in progress'. But they should give you a better idea of how
DDEML can be used from a C++ application.)
The two classes, DDE and DDEClient, can be found in DDE.H
and DDE.CPP. Class DDE contains all the basic facilities that
are needed by a DDE conversation, including the instance handle
and a pointer to the callback function (which an application
that uses the class has to provide). DDEClient relies on the DDE
class for these, and also provides member function to connect to
a DDE server and make a request for data. While I didn't have
time to implemenent member functions that can set up advise
loops (hot and warm links), it'd be simple enough to add them
using the facilities already built into DDEClient.
Both classes compile under Borland C++ 2.0 -- the latest
Borland compiler available at the time this column was
written. (Borland C++ 3.0 was still in Beta, and Microsoft C 7.0
had not reached beta.) Also, both DDEClient and
the DDE class rely on some application classes that I wrote and
have been using over the past year. However, you can
easily reconfigure both of these classes to work with an
application class like Borland's ObjectWindows.
To demonstrate these DDE classes, I wrote a program,
DDECLNT.EXE, which uses DDEML to connect to any DDE
server and retrieve data from it. For example, to connect to
Excel and retrieve rows 1-3, columns 1-4 from ANNUAL.XLS (a
worksheet that comes with Excel):
o Load Excel
o Use File|Load to load ANNUAL.XLS
o Load DDECLNT
o In DDECLNT's window (a single modal dialog),
enter "Excel" in the 'Server Name' field, "ANNUAL.XLS" in
the 'Topic Name' field, and "r1c1:r3c4" in the 'Item Name'
field.
o Press the 'Request' button.
This will cause DDECLNT to create a DDEClient object,
DDEClient newClient(Bufs[0].buffer, Bufs[1].buffer,
(FARPROC)DDEClientCallBack);
and pass it the server name and topic name as the first two
parameters, with the address of a callback function as the last
parameter.
To make a data request, DDECLNT.EXE only needs to call the
Request member function:
newClient.Request(Bufs[2].buffer);
Then, if a call to newClient.GetResult returns TRUE, the program
retrieves the data from newClient with a call to its GetData
member function,
SetDlgItemText(hDlg,RESULT_DATA,(LPSTR)newClient.GetData());
and puts the data directly into one of the data fields in the
main program's dialog.
The DDE and DDEClient classes do all the work.
I've tested the program under Win3.0 and Win3.1. I can't
guarantee it until Win3.1 and the final version of DDEML.DLL are
available, but you shouldn't have any problems with it. By the
way: considering how much DDECLNT.EXE does, I'm surprised that
it's only a little over 10k (of course, the 36k
DDEML.DLL has to be available, too).
Conclusion
As you can see, the DDEML is a powerful addition to Windows 3.1,
but there's a lot to it. C++ certainly makes DDEML easier to
deal with. I'd like to re-visit DDE again in future columns,
once Win3.1 arrives and these classes have time to mature. But,
until next month, enjoy!
===========================================
[figure 1]
Session Management
------------------------
DdeInitialize Registers an application with the DDEML
DdeUninitialize Frees an application's DDEML resources
Conversation Management
------------------------
DdeConnect Establishes a conversation with a server
DdeConnectList Establishes multiple DDE conversations
DdeDisconnect Terminates a DDE conversation
DdeDisconnectList Destroys a DDE conversation list
DdeEnableCallback Enables or disables one or more DDE conversations
DdePostAdvise Prompts a server to send advise data to a client
DdeQueryConvInfo Retrieves information about a DDE conversation
DdeQueryNextServer Obtains the next handle in a conversation list
Transaction Management
------------------------
DdeAbandonTransaction Abandons an asynchronous transaction
DdeClientTransaction Begins a DDE data transaction
Data Management
------------------------
DdeAccessData Accesses a DDE global memory object
DdeAddData Adds data to a DDE global memory object
DdeCreateDataHandle Creates a DDE data handle
DdeFreeDataHandle Frees a global memory object
DdeGetData Copies data from a global memory object to a buffer
DdeUnaccessData Frees a DDE global memory object
String Management
------------------------
DdeCmpStringHandles Compares two DDE string handles
DdeCreateStringHandle Creates a DDE string handle
DdeFreeStringHandle Frees a DDE string handle
DdeKeepStringHandle Increments the use count for a string
handle DdeQueryString Copies string-handle text to a buffer
Miscellaneous Functions
------------------------
DdeGetLastError Returns the error code set by a DDEML function
DdeNameService Registers or unregisters server name(s)
DdeSetUserHandle Associates a user-defined handle with a transaction
=================================================================